﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Sockets;

//资源更新的管理类
//继承MonoBehaviour是为了省事，可以直接用协程，其实这个类作为静态类比较好
public class ResourceUpdater : MonoBehaviour {
	//单例部分
	public static ResourceUpdater Instance{ get; private set;}
	static ResourceUpdater(){
		GameObject obj = new GameObject ("ResourceUpdater");
		DontDestroyOnLoad (obj);
		Instance = obj.AddComponent<ResourceUpdater> ();
	}

	//一些热更需要用到的数据，实际项目中直接写在这里并不是一个好习惯，但这里为了简单，就直接这样了
	//游戏服务器地址和端口，这里将从游戏服务器哪里获取当前最新的ResourceMap的MD5值，然后和本地的对应文件进行对比，决定是否需要更新ResourceMap
	#if UNITY_EDITOR
	private string GameServerIP = "127.0.0.1"; 	//本机IP
	#else
	private string GameServerIP = "192.168.1.104"; 	//测试用的临时IP，应该改为当前测试服务器的IP
	#endif
	private int GameServerPort = 9999;
	//文件服务器的地址和端口，这里将从文件服务器其中下载与本地有差异的文件并进行替换
	#if UNITY_EDITOR
	private string FileServerURL = "http://127.0.0.1:8888";	//本机IP
	#else
	private string FileServerURL = "http://192.168.1.104:8888";
	#endif
	private string currentPlatform = "invalid platform";

	//核心函数，文件对比和增量更新，异步更新，接收一个回调函数，更新过程中会将下载进度（0-100，不一定准确）通过这个函数传给调用者
	public void UpdateResource(Action<float> onProgress){
		//直接硬编码不同平台下的下载目录，必须与服务器那边一致（与本地打包目录可以不一致，但是最好一致）
		#if UNITY_ANDROID 
		currentPlatform = "Android";
		#elif UNITY_IOS
		currentPlatform = "IOS";
		#else
		currentPlatform = "Windows"
		#endif
		//计算本地的ResourceMap的MD5
		string currentMD5 = GetCurrentMD5();
		//给个伪进度
		onProgress(10);
		//从服务器那里获取当前的服务端的ResourceMap的MD5值
		var serverMD5 = GetResourceMapMD5(currentPlatform);
		Debug.LogFormat ("currentMD5 is : {0} , serverMD5 is : {1}", currentMD5, serverMD5);
		if (currentMD5 != null && serverMD5!=null && serverMD5.Equals(currentMD5,StringComparison.Ordinal)) {
			//已经最新，不需要热更新了
			onProgress (100);
			return;
		}
		Debug.Log ("Begin Hot Update");
		//开始热更新，先从文件服务器上下载最新的ResourceMap到内存中(避免覆盖掉原来的ResourceMap)
		DownLoadFileToMemery(FileServerURL + "/" + currentPlatform + "/ResourceMap",bytes=>{
			if(bytes == null){
				return;
			}
			//开始对比两者的差异，然后决定需要下载的文件
			List<string> bundlesToDownload = new List<string>();
			List<string> bundlesToDelete = new List<string>();
			GetDifference(bytes,bundlesToDownload,bundlesToDelete);
			//给个伪进度
			onProgress(25);
			if(bundlesToDownload.Count > 0){
				float currentProgress = 25;
				float progressStep = 76.0f/bundlesToDownload.Count;
				DownLoadFiles(bundlesToDownload,(result,path)=>{
					currentProgress += progressStep;
					onProgress(currentProgress);
				});
			}else{
				onProgress(100);
			}
			//保存Manifest bundle
			FileStream fs = new FileStream(Path.Combine(ResourceManager.Instance.AssetDataPath,"ResourceMap"),FileMode.Create,FileAccess.Write);
			fs.Write(bytes,0,bytes.Length);
			fs.Close();
			//删除不必要的文件
			foreach(var bundleName in bundlesToDelete){
				var path = Path.Combine(ResourceManager.Instance.AssetDataPath,bundleName);
				if(File.Exists(path)){
					File.Delete(path);
				}
			}
		});
	}

	//从游戏服务器获取当前的ResourceMap的MD5值
	private string GetResourceMapMD5(string platform){
		//直接TCP连接，为了省事，使用同步通讯的方式
		var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		try
		{
			//设个timeout，避免卡太久

			//clientSocket.SendTimeout = 1000;
			//一个坑，设置了ReceiveTimeout,在PC上没事，在Android上会导致异常，这异常属于Unity的BUG
			//可以参考此处https://answers.unity.com/questions/1441990/systemnetsocketssocketexception-operation-on-non-b.html
			//clientSocket.ReceiveTimeout = 3000;
			//同步连接，如果服务器挂了或者连不上，哪会导致客户端卡很久（也许是30秒？没试过），
			// 测试项目就凑合吧，正式项目还是不要这样玩 --
			clientSocket.Blocking = true;
			clientSocket.Connect(new IPEndPoint(IPAddress.Parse(GameServerIP), GameServerPort)); 
			//直接传输数据
			clientSocket.Send(Encoding.UTF8.GetBytes(platform));
			byte[] reciveBuffer = new byte[1024];
			int count = clientSocket.Receive(reciveBuffer);
			byte[] data = new byte[count];
			Array.Copy(reciveBuffer,data,count);
			return Encoding.UTF8.GetString(data);
		}
		catch (Exception e)
		{
			clientSocket.Close ();
			Debug.LogError("连接服务器失败" + e.ToString());
			return null;
		}
	}

	//从文件服务器上下载对应的文件，并将文件对应的二进制返回（不直接存到硬盘中）
	private void DownLoadFileToMemery(string url,Action<byte[]> onResult){
		//使用C#的http接口就好了
		try{
			HttpWebRequest request = HttpWebRequest.Create (url) as HttpWebRequest;
			HttpWebResponse response = request.GetResponse() as HttpWebResponse;
			//这个数据流就应该是我们要的文件二级制数据
			var responseStream = response.GetResponseStream();
			//假定这个文件比较小，不会超过1M
			byte[] fileBuffer = new byte[1024*1024];
			responseStream.BeginRead(fileBuffer,0,1024*1024,new AsyncCallback(ar=>{
				int count = responseStream.EndRead(ar);
				byte[] data = new byte[count];
				Array.Copy(fileBuffer,data,count);
				onResult(data);
			}),null);
		}catch(Exception e){
			Debug.LogError ( url + " 下载文件失败   \n" + e);
			onResult (null);
		}
	}
		
	//从文件服务器上下载对应的文件,异步下载，下载到指定的路径下，并且覆盖掉原来的同名文件（如果有的话）
	//没下载成功或者失败一个文件，都会调用一次传入的回调函数
	private void DownLoadFiles(List<string> bundleList,Action<bool,string> onOneFileDownload){
		foreach (var bundleName in bundleList) {
			try{
				var url = FileServerURL + "/" + currentPlatform + "/" + bundleName;
				HttpWebRequest request = HttpWebRequest.Create (url) as HttpWebRequest;
				HttpWebResponse response = request.GetResponse() as HttpWebResponse;
				//这个数据流就应该是我们要的文件二级制数据
				var responseStream = response.GetResponseStream();
				//本地文件路径
				FileStream fs = new FileStream(Path.Combine(ResourceManager.Instance.AssetDataPath,bundleName),FileMode.Create,FileAccess.Write);
				new HttpFileHandler (responseStream, fs, result =>{
					if(result){
						Debug.LogFormat("download bundle : {0} success, its path is : {1}",bundleName,Path.Combine(ResourceManager.Instance.AssetDataPath,bundleName));
						onOneFileDownload(true,Path.Combine(ResourceManager.Instance.AssetDataPath,bundleName));
					}else{
						Debug.LogFormat("download bundle : {0} failed",bundleName);
						onOneFileDownload(false,null);
					}
					fs.Close();
				}).StartWrite ();
			}catch(Exception e){
				Debug.Log ( bundleName + " 下载文件失败   \n" + e);
				onOneFileDownload (false,null);
			}
		}
	}

	//用于管理异步下载的类
	private class HttpFileHandler
	{
		private byte[] tempBuffer = new byte[1024];
		private Stream httpStream;
		private FileStream fileStream;
		private Action<bool> onResult;
		public HttpFileHandler(Stream httpStream,FileStream fileStream,Action<bool> onResult){
			this.httpStream = httpStream;
			this.fileStream = fileStream;
			this.onResult = onResult;
		}
		//异步将http中的文件数据写入到文件中的方法,异步下载，同步写入文件
		public void StartWrite(){
			try{
				httpStream.BeginRead (tempBuffer, 0, 1024, OnHttpReadFinish, null);
			}catch(Exception e){
				onResult (false);
				Debug.LogError (e);
			}
		}
		private void OnHttpReadFinish(IAsyncResult ar){
			int count = httpStream.EndRead (ar);
			if (count > 0) {
				fileStream.Write (tempBuffer, 0, count);
				StartWrite ();
			} else {
				onResult (true);
			}
		}
	}

	//计算当前ResourceMap的MD5值
	private string GetCurrentMD5(){
		var path = Path.Combine (ResourceManager.Instance.AssetDataPath, "ResourceMap");
		if (!File.Exists (path)) {
			if (File.Exists (Path.Combine (ResourceManager.Instance.StreamingDataPath, "ResourceMap"))) {
				File.Copy (Path.Combine (ResourceManager.Instance.StreamingDataPath, "ResourceMap"), path);
			} else {
				Debug.LogError ("ResourceMap Not Exists !");
				return null;
			}

		}
		return GetMD5HashFromFile(path);
	}

	//计算文件MD5值，网上随便抄的方法
	private static string GetMD5HashFromFile(string fileName)
	{
		try
		{
			FileStream file = new FileStream(fileName, FileMode.Open);
			System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
			byte[] retVal = md5.ComputeHash(file);
			file.Close();

			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < retVal.Length; i++)
			{
				sb.Append(retVal[i].ToString("x2"));
			}
			return sb.ToString();
		}
		catch (Exception ex)
		{
			throw new Exception("GetMD5HashFromFile() fail,error:" + ex.Message);
		}
	}

	private class BundleInfo
	{
		public Hash128 bundleHash;
		public bool isUsed = false;
		public BundleInfo(Hash128 hash){
			bundleHash = hash;
		}
	}
	//根据服务器和客户端bundleManifest来计算出需要更新（新增的也是更新）和删除的bundle列表
	public void GetDifference(Byte[] serverData,List<string> updateList,List<string> deleteList){
		updateList.Clear ();
		deleteList.Clear ();
		Dictionary<string,BundleInfo> clientBundleDic = new Dictionary<string, BundleInfo> ();
		var manifestBundle = ResourceManager.Instance.LoadBundleSync("ResourceMap");
		if (manifestBundle != null) {
			var clientManifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
			foreach (var bundleName in clientManifest.GetAllAssetBundles()) {
				clientBundleDic.Add (bundleName, new BundleInfo (clientManifest.GetAssetBundleHash (bundleName)));
			}
			//同时在内存中只能有一个同名的Bundle，所以这里先把client的Bundle卸载掉
			manifestBundle.Unload(true);
			//清掉一些缓存数据，非常丑陋的代码，正式项目中不应该出现。（正式项目中应该有对AssetBundle更合理地管理）
			if (ResourceManager.Instance.GetBundleMap ().ContainsKey ("ResourceMap")) {
				ResourceManager.Instance.GetBundleMap ().Remove ("ResourceMap");
			}
		}

		//假定下载成功，那么这些数据就直接当做AssetBundleManifest对应的数据来用
		manifestBundle = AssetBundle.LoadFromMemory(serverData);
		var serverManifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
		BundleInfo info = null;
		foreach (var bundleName in serverManifest.GetAllAssetBundles()) {
			if (clientBundleDic.TryGetValue (bundleName, out info)) {
				info.isUsed = true;	
				if (info.bundleHash != serverManifest.GetAssetBundleHash (bundleName)) {
					updateList.Add (bundleName);
				}
			} else {
				updateList.Add (bundleName);
			}
		}
		foreach(var pair in clientBundleDic){
			if (!pair.Value.isUsed) {
				deleteList.Add (pair.Key);
			}
		}
		//卸载掉Server的Bundle
		manifestBundle.Unload(true);
	}

}
